package com.xxl.job.admin.controller;

/**
 * @author songsong
 * @version 1.0
 * @date 2022/12/23 10:47
 **/

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.job.common.DTO.JobFlowDTO;
import com.job.common.DTO.NodeJobDTO;
import com.job.common.reference.TypeStore;
import com.xxl.job.admin.core.model.JobFlow;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.util.CookieUtil;
import com.xxl.job.admin.dao.JobFlowDao;
import com.xxl.job.admin.service.JobFlowService;
import com.xxl.job.admin.service.XxlJobService;
import com.xxl.job.admin.utils.NodeCheckUtil;
import com.xxl.job.core.biz.model.ReturnT;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

@Controller
@RequestMapping("/jobNode")
public class NodeJobController {
    @Resource
    XxlJobService xxlJobService;
    @Resource
    JobFlowDao jobFlowDao;
    @Resource
    JobFlowService jobFlowService;

    @RequestMapping("/getNodes")
    @ResponseBody
    public ReturnT getNodes() {
        //查找任务流程数，如果不为空，则组装成节点流程数据时用保存的节点位置信息
        JobFlow onlyOne = jobFlowDao.findOnlyOne();
        Map<String, JobFlowDTO.node> flowNodeMap = new HashMap<>();
        int flowId=0;
        if (onlyOne != null) {
            List<JobFlowDTO.node> nodeList = JSON.parseObject(onlyOne.getNodes(), TypeStore.LIST_JOB_FLOW_NODE_TYPE);
            flowNodeMap = nodeList.stream().collect(Collectors.toMap(JobFlowDTO.node::getBlockId, Function.identity()));
            flowId = onlyOne.getId();
        }
        List<XxlJobInfo> allJob = xxlJobService.getAllJob();
        NodeJobDTO dto = transform(allJob,flowNodeMap);
        dto.setJobFlowId(flowId);

        /*JobFlowDTO dto = new JobFlowDTO();
        BeanUtils.copyProperties(onlyOne,dto);
        if(CookieUtil.flag){
            List<XxlJobInfo> allJob = xxlJobService.getAllJob();
            //所有任务id集合
            List<String> jobIdList = allJob.stream().map(XxlJobInfo::getId).map(String::valueOf).collect(Collectors.toList());
            List<JobFlowDTO.node> nodeList = JSON.parseObject(onlyOne.getNodes(), TypeStore.LIST_JOB_FLOW_NODE_TYPE);
            //所有节点id集合
            List<String> nodeIdList = nodeList.stream().map(JobFlowDTO.node::getBlockId).collect(Collectors.toList());
            //nodeIdList与任务id集合的交集
            nodeIdList.retainAll(jobIdList);
            //任务id移除交集，剩下的为流程图需要增加的任务节点信息 jobIdList
            jobIdList.removeAll(nodeIdList);
            NodeJobDTO transform = transform(allJob.stream().filter(i -> jobIdList.contains(i.getId()+"")).collect(Collectors.toList()));
            dto.setNodeJobDTO(transform);

            List<String> node = nodeList.stream().map(JobFlowDTO.node::getBlockId).collect(Collectors.toList());
            //流程图节点id移除交集，剩下的为流程图需要删除的流程图节点信息 node
            node.removeAll(nodeIdList);
            dto.setRemoveIds(node);
        }*/
        return new ReturnT(dto);
    }

    @RequestMapping("/saveFlow")
    @ResponseBody
    public ReturnT saveFlow(JobFlowDTO jobFlowDTO) {
        //用户点击修改流程图后，点击提交，保存用户修改后的流程图，并将任务依赖关系更新（更新前需校验流程图是否有环）
        Map<Integer, NodeJobDTO.Node> nodeMap = buildFlowEdges(jobFlowDTO.getEdges());
        jobFlowService.updateJobFlow(nodeMap, jobFlowDTO);
        CookieUtil.flag=false;
        return ReturnT.ok("提交处理成功");

    }

    /**
     * 营造流程连接线数据
     *
     * @param edges
     */
    private Map<Integer, NodeJobDTO.Node> buildFlowEdges(String edges) {
        //转换成连线实体list
        List<JobFlowDTO.edge> edgeList = JSON.parseObject(edges, TypeStore.LIST_JOB_FLOW_EDGE_TYPE);
        //根节点id集合
        Set<Integer> rootIds = Sets.newHashSet();
        //根节点，节点map
        Map<Integer, NodeJobDTO.Node> nodeMap = Maps.newHashMap();
        //子节点集合
        Set<Integer> childIds = Sets.newHashSet();
        edgeList.forEach(item -> {
            //连接线上的起始节点
            Integer sourceId = Integer.valueOf(item.getPageSourceId());
            //如果添加成功，说明节点是第一次遍历到，需构建此节点对象信息,并添加到节点map中
            if (rootIds.add(sourceId)) {
                NodeJobDTO.Node node = new NodeJobDTO.Node();
                node.setId(sourceId);
                node.setChildJobIds(item.getPageTargetId());
                nodeMap.put(sourceId, node);
            } else {
                NodeJobDTO.Node node = nodeMap.get(sourceId);
                //如果是空，则说明是某个连接上的末端节点添加的节点信息
                if (StringUtils.isEmpty(node.getChildJobIds())) {
                    node.setChildJobIds(item.getPageTargetId());
                } else if (!node.getChildJobIds().contains(item.getPageTargetId())) {//判断此节点的子节点是否包含此末端节点，不包含就添加。
                    node.setChildJobIds(node.getChildJobIds() + "," + item.getPageTargetId());
                }
            }
            //连接线上的末端节点
            Integer targetId = Integer.valueOf(item.getPageTargetId());
            //如果添加成功，说明节点是第一次遍历到，需构建此节点对象信息,并添加到节点map中
            if (rootIds.add(targetId)) {
                NodeJobDTO.Node node = new NodeJobDTO.Node();
                node.setId(targetId);
                nodeMap.put(targetId, node);
            }
            childIds.add(targetId);
        });
        checkPath(rootIds,childIds,nodeMap);
        return nodeMap;
    }

    /**
     * 任务数据转换成节点数据结构
     *
     * @param allJob 任务数据
     * @return 节点数据
     */
    private NodeJobDTO transform(List<XxlJobInfo> allJob,Map<String, JobFlowDTO.node> flowNodeMap) {
        if (CollectionUtils.isEmpty(allJob)) {
            return null;
        }
        //根节点id集合
        Set<Integer> rootIds = Sets.newHashSet();
        //根节点，节点map
        Map<Integer, NodeJobDTO.Node> nodeMap = Maps.newHashMap();
        Set<Integer> childIds = Sets.newHashSet();
        //连接线数据集合
        List<NodeJobDTO.Edge> edges = Lists.newArrayList();
        //节点数据集合
        List<NodeJobDTO.Node> nodes = Lists.newArrayList();
        allJob.forEach(item -> {
            // 初始阶段，每一个点都设为顶点
            rootIds.add(item.getId());
            NodeJobDTO.Node node = new NodeJobDTO.Node();
            node.setId(item.getId());
            node.setJobDesc(item.getJobDesc());
            node.setChildJobIds(item.getChildJobId());
            //设置每个节点的位置信息
            if (MapUtils.isNotEmpty(flowNodeMap)) {
                JobFlowDTO.node flowNode=flowNodeMap.get(item.getId()+"");
                if (flowNode != null) {
                    BeanUtils.copyProperties(flowNode,node);
                }
            }
            nodes.add(node);
            nodeMap.put(item.getId(), node);
            if (StringUtils.isNotEmpty(item.getChildJobId())) {
                String[] childJobIds = item.getChildJobId().split(",");
                for (int i = 0; i < childJobIds.length; i++) {
                    int childJobId = Integer.valueOf(childJobIds[i]);
                    NodeJobDTO.Edge edge = new NodeJobDTO.Edge(item.getId(), childJobId);
                    edges.add(edge);
                    childIds.add(childJobId);
                }
            }
        });
        //校验是否有环
        checkPath(rootIds, childIds, nodeMap);
        //修正节点的层级数据
        amendLayer(nodes, nodeMap);

        //返回数据
        NodeJobDTO dto = new NodeJobDTO();
        dto.setNodes(nodes);
        dto.setEdges(edges);
        return dto;
    }

    /**
     * 校验节点信息
     * @param rootIds 节点id集合
     * @param childIds  子节点id集合
     * @param nodeMap   节点map
     */
    private void checkPath(Set<Integer> rootIds,Set<Integer> childIds,Map<Integer, NodeJobDTO.Node> nodeMap){
        //初始节点个数
        int size = rootIds.size();
        //只要是子节点都不可能是跟节点，移除。
        rootIds.removeAll(childIds);
        //任务数据最少有一个根节点
        if (rootIds.isEmpty()) {
            throw new RuntimeException("任务数据依赖关系有误");
        }
        // 记录遍历过的所有节点 ID
        HashSet<Integer> traversalNodeIds = Sets.newHashSet();
        //遍历根节点，检查从根节点往下的路径合法性，并设置层级数据
        rootIds.forEach(rootId -> {
            if (NodeCheckUtil.invalidPath(rootId, Sets.newHashSet(), traversalNodeIds, nodeMap)) {
                //进入，则校验失败
                throw new RuntimeException("任务数据路径上有环");
            }
            //设置节点的层数据
            buildLayer(rootId, 1, nodeMap);
        });
        // 理论上应该遍历过图中的所有节点，如果不相等则说明有环 (孤立的环)
        if (size != traversalNodeIds.size()) {
            throw new RuntimeException("任务数据路径上有独立的环");
        }
    }

    /**
     * 修正节点的层数据
     *
     * @param nodes   节点集合
     * @param nodeMap key为节点id，value为节点的map
     */
    private void amendLayer(List<NodeJobDTO.Node> nodes, Map<Integer, NodeJobDTO.Node> nodeMap) {
        /**
         * eg:a-b-c-d-e 路径上的abcde层级分别为12345，f-e路径上的fe层级分别为12，
         *  buildLayer方法会蒋e的层级设置为5，fe层级为15
         *  fe层级跨度太大，为了流程图的紧凑性，需将f的层级数据修正为4
         *  逻辑：
         * 遍历节点数据
         * 根据map得到节点的所有子节点数据，得到子节点的最小值的层级数据
         * 设置当前节点的层级数据（规则：小于所有子节点的最小层级数）
         */
        nodes.forEach(node -> {
            if (StringUtils.isEmpty(node.getChildJobIds())) {
                return;
            }
            Integer minLayer = nodes.size();
            //从节点map中获得此节点的子节点信息，遍历，然后递归设置节点的层数据
            String[] childJobIds = node.getChildJobIds().split(",");
            for (int i = 0; i < childJobIds.length; i++) {
                int childJobId = Integer.valueOf(childJobIds[i]);
                NodeJobDTO.Node childNode = nodeMap.get(childJobId);
                minLayer = Math.min(minLayer, childNode.getLayer());
            }
            node.setLayer(minLayer - 1);
        });
    }

    /**
     * 建立层级数据
     *
     * @param parentId 父节点id
     * @param layer    层级
     * @param nodeMap  节点map
     */
    private void buildLayer(Integer parentId, int layer, Map<Integer, NodeJobDTO.Node> nodeMap) {
        NodeJobDTO.Node parentNode = nodeMap.get(parentId);
        //此节点在多个路径上，会有多个层数据，取最大值。
        //eg:a->b->c,此路径abc的层级分别为123，a->c此路径ac的层级为12，综上，c的层级应为3
        int maxLayer = Math.max(parentNode.getLayer(), layer);
        parentNode.setLayer(maxLayer);
        if (StringUtils.isEmpty(parentNode.getChildJobIds())) {
            return;
        }
        //从节点map中获得此节点的子节点信息，遍历，然后递归设置节点的层数据
        String[] childJobIds = parentNode.getChildJobIds().split(",");
        for (int i = 0; i < childJobIds.length; i++) {
            int childJobId = Integer.valueOf(childJobIds[i]);
            buildLayer(childJobId, maxLayer + 1, nodeMap);
        }
    }

    @RequestMapping
    public String index() {
        return "jobNode/jobNode.detail";
    }
}
